home *** CD-ROM | disk | FTP | other *** search
Text File | 1992-04-08 | 43.6 KB | 1,138 lines | [TEXT/MWII] |
- all About the minilisp application
-
- Ruben Kleiman
-
-
- Minor editing by slm 3/10/92.
-
-
- If you're really interested in the nitty-gritty
- details of developing a program using MCL,
- this is your opportunity to learn by doing.
- We'll describe here how to build key features
- of the sample program Mini-Application. The
- complete source code for this program is also
- contained on this disc.
-
- Our sample application combines some of the features
- of programs like MacDraw and HyperCard. Its main
- components are:
-
- • Windows in which the user can build
- graphics
-
- • Palettes from which tools can be selected
- and from which graphic objects can be
- dragged out
-
- • A variety of graphic objects (for example,
- buttons, fields) that can be dragged from
- the palettes to build something in the
- windows
-
- • Menus to simplify the user interface
-
- • An event system that approximates
- HyperCard’s, including the ability to write
- scripts for objects - albeit in Lisp
-
- You may want to run the application before learning
- how it was built. Instructions follow in "To Run the
- Application." Starting with the section "Creating the
- Framework," we show how to build the application in a
- top-down manner, paying attention to the features of
- the development environment and of Lisp that enhance
- programmer productivity (and enjoyment). We craft the
- key methods or functions for each aspect of the
- application. We don't provide a complete exegesis of
- the application, but touch on every component
- mentioned above, implementing the main code for that
- feature.
-
- to run the application
-
- To run the application, you need a copy of Macintosh
- Common Lisp, Version 2.0, available from
- APDA. MCL runs in 4 megabytes.
-
- To run the application, open the file Read Me First.lisp
- into a text window in MCL and read the instructions.
- All you have to do is to set up the name of the
- directory where you installed the Mini-Application
- folder supplied on the CD ROM and evaluate the text
- window. This will cause the application to load and
- run.
-
- The application consists of the following files:
-
- File Name Description
-
- Read Me First.lisp The code that loads and runs
- the application. It also initializes
- objects needed by each run.
-
- mini-application-loader.lisp Loads the application code.
-
- compile-mini-application.lisp A file which, when loaded,
- recompiles the application
-
- globals.lisp Declares all globals used in the
- application.
-
- draw-dialog-class.lisp The class of windows and all
- their methods.
-
- palette-class.lisp The class of palettes and all
- their methods.
-
- draw-item-class.lisp The classes of all graphic
- objects and all their methods.
-
- menus.lisp Defines the menus and menu
- items used by this application,
- as well as the functions they
- invoke.
-
- default-handlers.lisp Defines the default behavior of
- the HyperCard-like script
- handlers, like MouseDown.
-
- utilities.lisp Utility functions used
- throughout.
-
- resources Contains a few resources (for
- example, icons).
-
-
- Creating The Framework
-
- As we begin to write our application, we open a MCL
- text document in which to enter and save the code. To
- do this, go to the File menu and choose New. A
- document called New will be opened.
-
- We will run our application by calling the function
- start-application, which will bring up the necessary
- menus and display a greeting. Type the following into
- our empty document:
-
- (defun start-application ()
- (when (welcome-accepted?) ; Display welcome message
- (show-menus) ; Initialize the menubar
- (print “Welcome!”))) ; Print greeting
-
- You can begin each line with a tab, and the editor will
- automatically indent your code to the appropriate
- level. The defun function is how you define a function.
- This consists of a function name (first argument), a
- list that specifies the arguments assumed by the
- function (second argument, also known as the “lambda
- list”), and any number of Lisp expressions (rest of
- arguments, also known as the body). Evaluating the
- function returns the value of the last expression in the
- body. In start-application, the lambda list has nothing
- in it, so the function does not take any arguments. The
- body consists of a conditional expression that calls
- the function welcome-accepted? and calls show-
- menus if that function returns a value other than NIL.
- All lines beginning with a semicolon are comments.
-
- To compile start-application, select the definition
- with the mouse and choose Eval Selection from the
- Eval menu. The compiler will print out a warning in the
- Listener to the effect that welcome-accepted? and
- show-menus are undefined. Lisp was tolerant: it
- could have given you an error! Don’t worry about this:
- when you define those functions, everything will be ok.
- Now save your text by choosing Save As... from the File
- menu.
-
- Our welcome dialog will display a string identifying
- the application and give the user an opportunity to
- cancel. This is straightforward with the predefined
- function y-or-n-dialog. Enter the following definition
- into your text window:
-
- (defun welcome-accepted? ()
- (y-or-n-dialog "Welcome To My App. Ready, Set, Go?"
- :yes-text "Go Dude!"
- :no-text "Nope"))
-
- y-or-n-dialog accepts as its first argument our
- message, and optional keyword arguments called :yes-
- text and :no-text are assigned the values “Go Dude!”
- and "Nope," respectively. The latter are the text
- assigned to buttons on the welcome dialog. Since the
- call to y-or-n-dialog does not depend on any
- variables, we can immediately test welcome-
- accepted? by selecting the text containing the call to
- y-or-n-dialog and then choosing Eval Selection from
- the Eval menu. The dialog should be shown. If you
- select the Go Dude! button, the function should return T
- (Lisp’s Boolean for true). When you evaluate any
- expression either in the Listener or in a text window,
- the result of the evaluation is returned in the Listener.
- Select the Go Dude! button to see what’s returned.
-
- Creating Our Menus
-
- The function show-menus is invoked by start-
- application when the welcome message is accepted. In
- our application, show-menus will eliminate the
- normal MCL menus and display a menu bar with the
- following menus: the standard File, Edit, and Window
- menus, an Options menu, and a menu that provides
- information about the currently selected graphic
- object. For purposes of this demonstration, we will
- limit ourselves to building the File menu. Our File
- menu will have New, Close, and Quit menu items (we
- won’t support Save). See the source code for the
- definitions of the Close and Quit menu items.
-
- Type and evaluate this expression in a text window:
-
- (defvar *mini-application-file-menu*
- (make-instance 'menu :menu-title "File"))
-
- First, defvar is the Lisp function that lets you define
- and initialize a global variable: we have thus
- initialized the variable *mini-application-file-menu*
- to contain the menu instance. The method make-
- instance, which is similar to New in SmallTalk and in
- MacApp, enables you to create an instance from a
- class, and to optionally initialize some of its slots.
- (The term slot is synonymous with instance variable in
- other object-based languages.) The first argument to
- make-instance is the name of the class, and the
- subsequent arguments are keyword-value pairs with
- optional initializations that depend on the class.
- Above, the predefined MCL class menu is instantiated
- and the :menu-title keyword option is initialized with
- the name of the menu. (The name menu is preceded
- with a single quotation mark to indicate that we are
- referring to the symbol menu rather than to the
- variable menu.)
-
- To test this instance, you can invoke the menu-install
- method on it:
-
- (menu-install *mini-application-file-menu*)
-
- The menu bar should now include a new File menu on
- the far right. Clicking on the new File menu will not
- yield any menu items, so let’s define one. Evaluate the
- following:
-
- (defvar *file-new-menu-item*
- (make-instance 'menu-item
- :menu-item-title "New"
- :command-key #\N
- :menu-item-action 'new-menu-item-action))
-
- The predefined MCL class menu-item supports,
- among other things, an optional command-key
- character - characters in Lisp are represented by
- prefixing the character with #\ - and a menu item
- action. The latter is a function that will be called
- when the menu item is chosen. In this example, we
- define the keystroke combination Command-N to be
- equivalent to choosing the menu item. We have also
- given new-menu-item-action as the name of the
- function that should be called when this menu item is
- chosen - new-menu-item-action is defined in the
- section “Creating Our Window”. The additional
- keywords :disabled, :menu-item-color, :menu-item-
- checked, and :style permit you to specify whether the
- menu item should be disabled, the colors of the parts
- of the menu (such as background, text), whether the
- item should be checked, and its style.
-
- We are ready to install the new menu item on the File
- menu. Type and evaluate the following in a text
- window:
-
- (add-menu-items *mini-application-file-menu*
- *file-new-menu-item*)
-
- add-menu-items is a predefined MCL method for
- menus that enables us to add one or more menu items
- to a menu—whether the menu is installed or not. If you
- pull down the new File menu, you should see the New
- item. However, if you choose this item, you should see
- an error report in the Listener telling you that new-
- menu-item-action is undefined. Lisp is forgiving but
- not clairvoyant!
-
- Now that we know how to define menus and menu
- items, we want to define a menu bar so that we can
- treat our application’s menus as a unit. First, let’s
- deinstall our File menu by evaluating the following:
-
- (menu-deinstall *mini-application-file-menu*)
-
- A menu bar consists of a list of menus that will be
- displayed in a left-to-right order. The complete source
- code for this application defines all menus and menu
- items, but here we will restrict ourselves to the File
- and Windows menus. We define the menu bar by
- evaluating this expression:
-
- (defvar *mini-application-menubar*
- (list *mini-application-file-menu*
- *windows-menu*)))
-
- The convenient variable *windows-menu* is bound by
- MCL to the menu that MCL already uses in its menu
- bar, so we add it here to get that functionality for
- free. To install our menu bar, evaluate this:
-
- (set-menubar *mini-application-menubar*)
-
- set-menubar deinstalls the MCL menubar and
- installs ours in its place. MCL provides a set of
- methods that can be invoked at any time on menus or
- menu items. For example, examine the New item after
- evaluating each of the following lines:
-
- (set-menu-item-check-mark *file-new-menu-item* t) ; Put a check mark
-
- (set-menu-item-style *file-new-menu-item* :outline) ; Outline font
-
- (set-menu-item-title *file-new-menu-item* "Was New") ; Other title
-
- (menu-item-disable *file-new-menu-item*) ; Disable the menu item
-
- MCL binds its own menu bar to the global *default-
- menubar* (never leave home without it!), so to get
- back to MCL’s menu bar, evaluate:
-
- (set-menubar *default-menubar*)
-
- Finally, we want to define the function show-menus,
- which will be called by start-application when our
- application needs to be started. This is simple:
-
- (defun show-menus ()
- (set-menubar *mini-application-menubar*))
-
- The complete source code defines *mini-application-
- menubar* to include all of the necessary menus and
- menu items to run the application.
-
- Creating Our Window
-
- To see how windows are handled, let’s start right off
- by creating one. Evaluate:
-
- (setq my-window (make-instance 'window))
-
- You should immediately see a window named Untitled
- appear on your main screen . Let’s see what we can do
- with windows. Examine the window after evaluating
- each of the following lines:
-
- (set-window-title my-window "d e v e l o p") ; Change window title
-
- (set-view-size my-window 200 200) ; Change window size
-
- (set-view-position my-window 150 50) ; Change window position
-
- set-window-title, set-view-size, and other functions
- used here and in other places are actually methods:
- they are sensitive to the types of objects being passed
- to them as arguments. As with other object-based
- languages, Lisp methods decide what to do based on the
- class of which the object is an instance. Unlike other
- object-based systems that only look at the class of
- the first argument, the Common Lisp Object System
- (CLOS) considers the classes of all of its arguments
- before deciding what to do. It is arguable just how
- useful this really is, but if you ever need this
- capability, it’s there! CLOS is implemented such that
- you won’t pay for this feature unless you use it.
- Methods are defined using the function defmethod,
- while regular functions are defined using defun.
-
- In our section on menus and in this section, we’ve seen
- that MCL uses the CLOS system to implement its user
- interface with a variety of methods — for example, add-
- menu-items, set-view-position. Fortunately, these
- methods are also available to us, the users. One other
- benefit of the MCL user interface is its view
- architecture and, in addition to windows, a useful
- variety of predefined views like radio buttons, check
- boxes, and editable text. Let’s understand MCL’s view
- system so that we can exploit it for our application.
-
- The view architecture allows one to define graphic
- objects as views. A view has its own coordinate
- system and origin relative to a view that contains it
- (called the view container); it is defined as a
- rectangular area determined by its position relative to
- its container and its size. It is thus easy to, for
- example, independently scroll separate views. A view
- can be a window, a radio button, or anything that can
- be displayed on the screen.
-
- We will build our application windows with the help of
- the view system. However, we will retain some control
- over how views are displayed. For example, we want to
- control the back-to-front ordering of graphic objects
- in a window, to be able to move objects among
- windows, and to constrain the behavior of objects
- (such as tools) in palettes.
-
- Let’s create a subclass of the predefined window
- class that is customized for our application. This class
- will include a slot my-items that will hold a list of
- all the graphic objects that we draw into the window,
- remembering their back-to-front ordering. We start by
- creating a subclass of the window class named draw-
- dialog. defclass is the CLOS function for defining a
- class:
-
- (defclass draw-dialog (window)
- ((my-items :initform NIL) ; List of all items in window
- (item-last-under-mouse :initform NIL) ; Item currently under the mouse
- (browse-mode :initform nil) ; Mode in which window is being used (default = author)
- (selections :initform nil) ; Currently selected item(s)
- (create-by-rectangle :initform nil) ; Can draw-items be created by dragging out a rectangle?
- )
- (:documentation "This class defines our windows"))
-
- Our class draw-dialog adds five slots named my-items,
- browse-mode, item-last-under-mouse, selections and
- create-by-rectangle to its superclass, window. The first
- argument to defclass is the name of the class, draw-dialog,
- followed by a list with the names of all classes, if
- any, from which we want to inherit (that is, that we
- want our class to be a subclass of)—in our case, just
- the window class. Multiple inheritance is supported
- and although things are, by default, inherited in the
- order in which they appear in this list, protocols are
- available in CLOS for controlling this. The third and
- fourth arguments shown here are a list of descriptors
- for each new slot that we want to define and
- documentation text, respectively.
-
- A descriptor is a list that starts with the name of the
- slot followed by a set of keyword-value pairs that
- represent options concerning how the slots get
- initialized when an instance of this class is created.
- We used the :initform keyword followed by the
- expression NIL. This means that when an instance of
- draw-dialog is created, the my-items slot will by
- default be set to whatever the following expression
- evaluates to—that is, NIL.
-
- The draw-dialog class will inherit slots from the
- window class and from the classes it inherits from.
- Verify that this new subclass definition works by
- creating an instance of it:
-
- (setq my-window (make-instance 'draw-dialog))
-
- One way to check whether our new slots my-items,
- browse-mode, item-last-under-mouse have, selections and
- create-by-rectangle been added and initialized to the
- value NIL, is to get their slot values directly. These
- values can be obtained by evaluating the following
- expressions:
-
- (slot-value my-window 'my-items) ; Returns value of slot my-items
- (slot-value my-window 'browse-mode)
- (slot-value my-window 'item-last-under-mouse)
- (slot-value my-window 'selections)
- (slot-value my-window 'create-by-rectangle)
-
- Another way to check these slots is by using the Lisp
- inspector to view the object instance. The inspector
- provides us with a description of any object (this
- includes constants, variables, and functions) that we
- want to look at. (In addition, as of this writing, the
- released version of the MCL 2.0 inspector will allow
- you to directly edit values.) To inspect the instance in
- my-window, evaluate (inspect my-window), which
- brings up a screen that looks like Figure 1.
-
- Figure 1: [ See Figure 3 in develop, p104 ]
-
- Viewing an Instance of the draw-dialog Class in the
- Inspector
-
- The items following Local slots: are slots of the
- object bound to my-window. The slots that we added
- should be at the top, as shown; the remaining slots
- have been inherited from the window class. For
- example, the slot wptr contains the pointer to the
- Macintosh window definition block, and view-size is
- the point (that is, a long used as a point) for the
- window’s size. You should resist the temptation to
- directly access these slots, unless you absolutely need
- to. The predefined window methods, like set-view-
- size, should be used instead. If you’d like to inspect
- the window block itself, you can double-click on the
- line in Figure 1 with the wptr to get the display shown
- in Figure 2.
-
- Figure 2: [ See Figure 4 in develop, p105 ]
-
- Viewing the Window Record for Our Instance of
- draw-dialog in the Inspector
-
- You could continue this process indefinitely—for
- example, looking next at the rectangle records in the
- window record. As you look at these lower-level
- structures, you begin to see that although Lisp is a
- very high-level language, you can still access the
- system as if writing in assembler language. As we
- shall see later, this clarifies toolbox access
- considerably when compared with C and Pascal.
-
- Now, we want to define the behavior of our
- window—what happens when it is created, when it is
- closed, when things are added to it, when the mouse is
- clicked on it, and so on. Fortunately, much of this
- behavior is already handled for us by the predefined
- window methods.
-
- In our menu section above, we defined for the File
- menu the New menu item which, when chosen, called
- the function new-menu-item-action. We are now
- ready to define this function, which creates an
- instance of a draw-dialog window:
-
- (defvar *window-count* 0) ; For a new window’s title
-
- (defun new-menu-item-action ()
- (make-instance 'draw-dialog
- :window-title
- (format nil "Draw Dialog ~a" (incf *window-count*))
- :view-size (make-point (- *screen-width* 150)
- (- *screen-height* 100))
- :view-position (make-point 5 40)
- :color-p *color-available*
- :window-type :document-with-zoom))
-
- We used the Lisp format statement to create the
- window title. format is a sophisticated instrument
- not only for writing strings to any stream, but also for
- generating string expressions of arbitrary complexity.
- We use the :window-type option to require a
- document with zoom box. *screen-width* and
- *screen-height* are the main screen’s width and
- height, respectively, which are supplied by MCL.
- Finally, incf is a macro that lets us increment and set
- the value of *window-count*.
-
- One interesting behavior that we want to add to draw-
- dialog is to make the window handle events like
- HyperCard does. The concept is that an instance of
- draw-dialog, as well as any object in it, can be
- scripted. A script will here be defined as a set of
- handlers. Instead of having a scripting language like
- HyperTalk, we’ll use Lisp, which is handier. A handler
- will be a method whose name is the name of the
- handler. The following types of handlers will therefore
- be defined:
-
- idle Periodically called by the system.
-
- mouse-enter Called whenever the mouse enters the
- bounds of the object.
-
- mouse-leave Called whenever the mouse leaves the
- bounds of the object.
-
- mouse-within Periodically called while the mouse is
- within the bounds of the object.
-
- key Called whenever the object is selected
- and a key is pressed.
-
- mouse-down Called whenever the mouse goes down
- within the bounds of the object.
-
- mouse-up Called whenever the mouse goes up
- within the bounds of the object.
-
- All objects will have default handlers that do nothing,
- except for the window’s idle handler, which simply
- passes the idle event to objects in its window.
-
- We’ll define a set of default handlers that will be
- called if the user does not write any scripts for these
- handlers. Most of these will do nothing. Here, we
- implement the idle default handler for the draw-
- dialog class:
-
- (defmethod idle ((this-window draw-dialog))
- (dolist (item (slot-value this-window 'my-items))
- (idle item)))
-
- This method simply sends an idle message to each item
- in the my-item slot of the window. (Note how easily
- we have been able not only to talk about but also to
- show how we can control the items in our drag-dialog
- windows without having defined the objects
- themselves! This will be more dramatically clear as
- we proceed to provide the event-handling machinery
- that drives all of the events above.)
-
- This is a simple method definition. Its name is idle and
- it has only one formal argument: an instance of a
- draw-dialog class. During the execution of the
- method, this instance is bound to the local variable
- this-window. In many other object-based languages,
- this variable is called self, but in Common Lisp it must
- be explicitly bound to a variable in the formal
- argument list. Why? Because, as we mentioned earlier,
- a Common Lisp method will look at the class of every
- argument passed to it before deciding which function
- to call. Thus, there isn’t any distinguished self, but
- rather many selves competing for identityhood!
-
- Another thing to notice in idle is how a slot of the
- object is accessed. The construct
-
- (slot-value <object> <slot-name>)
-
- is the default way to refer to an object’s slot value.
- However, the :accessor option of defclass enables you
- to define how you want to name the slot accessor. For
- example:
-
- (defclass person
- ((age :initform 20
- :accessor age)))
-
- enables you to access the age slot of an instance x of
- person via the expression (age x). Named accessors
- have the virtue of hiding the object system and are
- encouraged. However, we will consistently use slot-
- value to access slot values in order to clarify our use
- of object slots in our tutorial.
-
- Next, we want to drive the event system. From where?
- Unlike in other Macintosh programming environments,
- when we write a Lisp program we are not responsible
- for calling WaitNextEvent. Fortunately, the MCL
- window class has a predefined method called
- window-null-event-handler that is periodically
- called by MCL; in particular, after each
- WaitNextEvent is processed. MCL will apply this
- method to any active instance of a window class of
- which it is aware. This includes all instances of
- draw-dialog, since the latter is a subclass of
- window, giving us one of the hooks we needed. Let’s
- define window-null-event-handler for draw-dialog
- to handle the event traffic as follows:
-
- (defmethod window-null-event-handler ((w draw-dialog))
- (let* ((where (view-mouse-position w)) ; Window coordinate point of mouse
- (item (find-view-containing-point w (point-h where) (point-v where)))
- (last-under-mouse (slot-value w 'item-last-under-mouse)))
- ;; Handle mouse-within, mouse-enter and mouse-leave events
- (when (and (slot-value w 'browse-mode) ; in browser mode and
- item) ; when mouse is over an item
- (cond ((eq last-under-mouse item)
- (mouse-within item where))
- (t (if last-under-mouse
- (mouse-leave last-under-mouse where))
- (setf (slot-value w 'item-last-under-mouse) item)
- (mouse-enter item where)))
- ;; Handle idle event for window
- (idle w)))
- (call-next-method))
-
- Here, we bound the variable w to the instance of the
- draw-dialog window to which the message was sent.
- The let* statement creates the following local
- variables:
-
- where The current location of the mouse as a
- point.
-
- item The item (either the window or an item
- in the window) under the mouse.
-
- last-under-mouse
- The item that was under the mouse
- last time we checked.
-
- The MCL method view-mouse-position applied to our
- window returns to us the position of the mouse in
- local window coordinates. The MCL method find-
- view-containing-point returns the most specific
- view under the mouse: either an item in the window, or
- the window itself if the mouse isn’t over an item.
- Since we plan to treat our draw items as views (that
- is, as subviews of the window they are contained in),
- we will for now let find-view-containing-point do
- the work for us. Finally, the item-last-under-mouse
- slot is extracted here for performance since we’ll
- make multiple references to it.
-
- The cond statement figures out which of and to whom
- the following messages should be sent: mouse-enter,
- mouse-within, or mouse-leave. We check the value of
- the window’s slot browse-mode to see if the window
- is in browse or author mode. We only want to dispatch
- the events when in browse mode.
-
- Note how the slot item-last-under-mouse is set in
- that conditional. The general expression for setting a
- slot value is:
-
- (setf (slot-value <object> <slot-name>) <value>)
-
- The setf function is a generalization of setq and can
- therefore be used in its place. setf enables you to
- make an expression evaluate to the value you want.
- More precisely, evaluating
-
- (setf <list-or-symbol> <value>)
-
- implies that evaluating <list-or-symbol> will return
- <value>. For symbols, this is always the case. For
- lists, this is only possible for certain system-defined
- functions, like slot-value. But you are free to define
- any function to behave this way: consult define-setf-
- method in your friendliest Common Lisp book.
-
- Next to last, we call the idle method in the window. We
- have defined this method above: it distributes the idle
- event to all items in it. Finally, we call call-next-
- method, which is the way CLOS enables us to call the
- window-null-event-handler method that draw-
- dialog would otherwise have inherited from the class
- window. If we wanted to override it completely, we
- would not have called call-next-method. The latter is
- similar to MacApp’s inherit- or SmallTalk’s CallSuper
- methods.
-
- We’re only left with mouse-down, mouse-up, and
- key events to worry about. We’re lucky again, because
- MCL is always busy in the background dispatching
- calls to the MCL methods view-click-event-handler,
- window-mouse-up-event-handler, and view-key-
- event-handler, which are called after the mouse goes
- down or up, or after a keystroke, respectively. These
- methods are a breeze to code:
-
- (defmethod view-click-event-handler ((w draw-dialog) where)
- (let ((item (find-view-containing-point
- w (point-h where) (point-v where))))
- (if (slot-value w 'browse-mode)
- (if item
- (mouse-down item where))
- (author-mode-click-handler item where))))
-
- (defmethod window-mouse-up-event-handler ((w draw-dialog))
- (let* ((where (view-mouse-position w))
- (item (find-view-containing-point
- w (point-h where) (point-v where))))
- (if (and item (slot-value w 'browse-mode))
- (mouse-up item where)))
- (call-next-method))
-
- (defmethod view-key-event-handler ((w draw-dialog) character)
- (let* ((where (view-mouse-position w))
- (item (find-view-containing-point
- w (point-h where) (point-v where))))
- (if (and item (slot-value w 'browse-mode))
- (key item character)))
- (call-next-method))
-
- The mouse position is supplied by MCL in view-click-
- event-handler, which is called when the mouse is
- pressed down; in the other cases we find it using
- view-mouse-position. For the view-key-event-
- handler MCL passes the character for the key that
- was pressed. Again, we call call-next-method in case
- it does anything important. The method author-mode-
- click-handler called by view-click-event-handler
- will take care of object selection, deselection,
- resizing, and dragging operations similarly to
- HyperCard. Note that this method is invoked on the
- item, which could be the window itself or one of the
- graphic items in it. The latter version is described in
- the section “Creating Draw Items,” while the
- definition for the window version is:
-
- (defmethod author-mode-click-handler ((w draw-dialog) where)
- (if (double-click-p)
- (author-mode-double-click-handler w where)
- (author-mode-single-click-handler w where)))
-
- double-click-p is a MCL function that tells us
- whether we have a double click. The method author-
- mode-double-click-handler (not shown in this
- article) will be responsible for presenting a dialog box
- with information about the window, while:
-
- (defmethod author-mode-single-click-handler ((w draw-dialog) where)
- (when (eq w (find-view-containing-point
- w (point-h where) (point-v where))
- (deselect-items w)))
-
- The latter method will deselect everything when the
- mouse is clicked over the window and there are no
- items under it. The method deselect-items is
- straightforward:
-
- (defmethod deselect-items ((window draw-dialog))
- (dolist (item (slot-value window 'selections))
- (setf (slot-value item 'selected) nil)
- (view-draw-contents item)) ; Redraw item
- (setf (slot-value window 'selections) nil))
-
- We cycle through each item that has been put into the
- selections slot of the window and turn off its
- selected flag (see the “Creating Draw Items” section),
- redraw the item, and then delete the selections slot.
-
- As we define more functions and methods, our text
- windows are becoming rather large. Fortunately, we
- can go to the MCL Toolsmenu and choose List
- Definitions to get the window shown in Figure 3. Each
- line in this window shows a function, method, and
- other kinds of definitions found in the text window. By
- double-clicking on any item, you can scroll the text to
- the right item. It is possible to list things
- alphabetically or in the order they appear in the
- window. One of these windows can be simultaneously
- opened per opened text window.
-
- Figure 3: [ See Figure 2 in develop, p98 ]
-
- The "List Definitions" Window
-
- We are done with the window class, except for a
- detail concerning find-view-containing-point. We
- would like it to take into account the back-to-front
- ordering of items in our window, but the view system
- will not necessarily be aware of it. Therefore, we will
- override this method by our own version, which will
- cycle through the items in the my-values slot of the
- draw-dialog class in the correct front-to-back order.
- [ Don't look for a definition of find-view-containing-point,
- you won't find it. The built-in version is used. It works
- fine. ]
-
- Creating Our Palette
-
- Our palette will have two kinds of objects in it:
-
- • Tools to select what should be done
-
- • Objects that can be dragged into our windows
-
- It seems that the draw-dialog class can do most of
- the work for us, so we will define a subclass of it
- called palette as follows:
-
- (defclass palette (draw-dialog)
- ((my-tools :initarg :tools)
- (my-draw-items :initarg :draw-items))
- (:documentation "Palettes used in our application"))
-
- The my-tools slot will contain all the items in the
- palette that can be viewed as tools, whereas the my-
- draw-items slot will contain all items that can be
- dragged out of the palette into other windows. These
- slots will be initialized with instances of tools and
- graphic items that will be used in any one session of
- our application. Once a palette is created and
- initialized, a layout method (in CD ROM sources) will
- look at these slots to figure out how to lay out the
- items—for example, tools first and draggable items
- next. These are convenience slots, since (as we’ll see)
- graphic items themselves know whether they are tools
- or not.
-
- We have to enforce the following differences between
- our palette and draw-dialog classes:
-
- • Since tools are not accessible to users, a
- convenient place to put the code that does
- the tool’s work when it is clicked is the
- tool’s mouse-down event handler.
-
- • Items in our palette cannot be moved
- within or resized in the palette.
-
- • Tools cannot be dragged out of the palette.
-
- First, we want to make sure that a palette’s tools get
- the mouse-down event whether or not we are in
- author mode. We can redefine view-click-event-
- handler this way:
-
- (defmethod view-click-event-handler ((palette palette) where)
- (let ((item (find-view-containing-point
- palette (point-h where) (point-v where))))
- (if (slot-value item 'tool)
- (mouse-down item where)) ; dispatch the mouse-down event
- (call-next-method))) ; proceed with the usual behavior
-
- If an item is selected and if it is a tool, we force the
- mouse-down and then call the draw-dialog’s view-
- click-event-handler method using call-next-method.
- The check (slot-value item 'tool) anticipates the
- definition for graphic items in the next section: the
- tool slot of an item tells it whether it is a tool or not.
-
- Finally, since the resizing and dragging is done by the
- items themselves, items that know themselves to be
- in the palette should not allow themselves to be
- dragged or resized around a palette (but they should let
- themselves be dragged to other windows!). Similarly,
- items that are tools will know better than to drag
- themselves out of a palette! This (and more) we’ll
- discover in the following section.
-
- Creating Draw Items
-
- This is our last and most substantial section, for the
- graphic items have been delegated most of the work by
- the other classes. We will define one basic class of
- graphic items from which tools and other kinds of user
- interface objects will derive.
-
- (defclass draw-item (dialog-item)
- ((rectangle :initarg :rect :initform nil)
- (tool :initarg :tool :initform nil)
- (selected :initform nil)
- (name :initarg :name :initform ""))
- (:documentation "The user interface objects"))
-
- We call our class draw-item. It will be a subclass of
- MCL’s dialog-item class. The latter class supports a
- variety of specializations for typical Macintosh user
- interface items, like radio and round buttons, check
- boxes, and static text. Since we don’t want to
- duplicate that functionality, we subclass from dialog-
- item. Since dialog-item itself inherits from the class
- view (actually, from simple-view—but let’s not split
- hairs here), we get all the functionality we need
- without using multiple inheritance!
-
- If you are using MCL 2.0 right now, you can verify this
- provenance by inspecting the class object for draw-
- item using the inspector. find-class returns to you the
- class object, given a class name:
-
- (inspect (find-class 'draw-item))
-
- When the inspector window comes up, double-click on
- the line ccl::precedence-list: and you will get the
- window shown in Figure 4.
-
- Figure 4: [ See Figure 5 in develop, p108 ]
-
- The Class Precedence List for draw-item
-
- You will notice that our draw-item not only inherits
- from simple-view, but also from stream. The latter
- class allows one to apply format, print, and other
- stream (input/output) methods to our items!
-
- draw-item’s rectangle slot will be used to keep the
- bounds of the draw-item. These bounds will be the
- same as one would obtain by using the methods view-
- position and view-size when applied to the item, but
- will be much more efficient than making two method
- calls. The tool slot tells us whether the item is a tool
- or not. The selected slot tells us whether the item is
- selected (note that this information is redundant since
- it is also maintained in the window’s selections slot).
- Finally, a name for the draw item.
-
- Now, the business for draw-item to attend to, as we
- recall, is as follows:
-
- • Implement the author-mode-click-
- handler method, which is responsible for
- resizing and dragging an item.
-
- • Implement a variety of useful subclasses
- of draw-items, like editable text fields.
-
- As we recall, the author-mode-click-handler method
- was called by the draw-dialog window’s view-click-
- event-handler (see “Creating Our Window”) when the
- mouse clicked on an item contained by the window
- when in author mode. Author mode allows us to resize
- and drag items in a window and, in our implementation,
- to even drag items between windows. Let’s define this
- method:
-
- (defmethod author-mode-single-click-handler
- ((item draw-item) mouse-loc)
- (if (mouse-moved (view-window item) mouse-loc)
- (if (resize? item mouse-loc)
- (resize item mouse-loc)
- (maybe-drag item mouse-loc)))
- (deselect-items (view-window item))
- (select-item item))
-
- (defun mouse-moved (window mouse-loc)
- (loop
- (cond ((not (mouse-down-p))
- (return nil))
- ((neq (view-mouse-position window) mouse-loc)
- (return t)))))
-
- The method view-window, when applied to a view,
- returns the window that contains it. First, we call
- mouse-moved to see if the mouse moved before it
- was released: if so, either a resize or drag is intended.
- We call resize? to test whether a resize is intended,
- and if so we call resize to do the resizing, or else we
- call maybe-drag. Why maybe-drag? Because we will
- not want to drag if the item is a tool. Let’s look at
- resizing first.
-
- resize? need only check that the mouse is touching the
- item’s direct manipulation “handles.” It must also
- deny resizing if the item is in a palette. For our
- purposes, the handles will be defined to be a
- rectangular frame around the bounds of the item, inset
- by a slop amount.
-
- (defmethod resize? ((item draw-item) current-mouse-loc)
- (when (neq (type-of (view-window item)) 'PALETTE)
- (rlet ((handles-rect :rect
- :topleft (view-position item)
- :bottomright (add-points
- (view-position item)
- (view-size item))))
- (inset-rect handles-rect *slop-in-pixels* *slop-in-pixels*)
- (not (point-in-rect-p handles-rect current-mouse-loc)))))
-
- The when statement checks whether the item is in a
- palette. Note the use of the Common Lisp type-of
- function to determine the class of the window
- containing the item! rlet is a kind of let provided by
- MCL that allows one to bind a variable to a Macintosh
- record (that is, a pointer to a record in the Macintosh
- heap). The rlet is efficient because it allocates the
- record in the stack instead of calling the Memory
- Manager. We bind a rectangle record (the keyword :rect
- specifies this) to the variable handles-rect. The
- keywords :topleft and :bottomright allow us to supply
- the item’s position, which is deduced using view-
- position and view-size methods. inset-rect is one of
- the QuickDraw utility functions available with MCL
- that enables us to inset the rectangle, and *slop-in-
- pixels* is one of our globals that we bind to a number
- like 4. Finally, if the mouse location is not in the inset
- rectangle, then the user wants to resize.
-
- The resize method illustrates the use of direct stack-
- based toolbox calls: these are the functions whose
- names are prefixed by the underscore (_) character,
- like _InvertRect. In these calls, you can only pass a
- pointer, a single word, or a double word. You must take
- all the precautions you would if you were calling the
- traps from assembler. As a matter of fact, you are at
- assembler level when you make these calls: you can
- pass pointers or handles or a long number in a double
- word: you’re the boss.
- [ A numbersign (#) is added so the trap (that's what toolbox
- calls are called) will be autoloaded on first use. ]
-
- Another important feature of Common Lisp to observe
- in resize is the unwind-protect construct. This
- enables you to protect an expression (in this case, the
- long expression enclosed within the loop) in case
- there’s an error during its execution. The unwind-
- protect guarantees that the expressions following the
- protected expression will be executed despite the
- error. We thus ensure that all the regions created for
- resize will be disposed of even if the routine crashes.
-
- The drag method creates a region around the bounds of
- the thing to be dragged and calls the function drag-
- inverted-region, which enables drags to occur
- between as well as within windows and returns an
- offset to where the item was dragged. If the
- destination window is the same as the window where
- the drag started and the window is a palette, we want
- to do nothing since we can’t have a drag within a
- palette. Otherwise, we adjust the position of the item
- using set-view-position. If the move is to another
- window, then we use move-item-to-window. The
- latter will interpret a drag as a clone of the item if
- the drag started on a palette and ended in a draw-
- dialog window, or else it will interpret it as a simple
- drag. move-item-to-window uses the MCL view
- methods add-subviews and remove-subviews to add
- and remove the items from the source and destination
- windows. If there’s a clone, the function clone-draw-
- item is called and the item is added to the destination
- window. move-item-to-window ends by selecting the
- destination window.
-
- Finally, we must define some useful draw-item
- subclasses. Since draw-item is a subclass of MCL’s
- dialog-item class, any dialog-item object of MCL
- can be a draw-item. For example, to define a check box
- draw-item:
-
- (defclass check-box (draw-item check-box-dialog-item)
- ())
-
- This creates a class from which check boxes can be
- instantiated. Note the multiple inheritance: check-box
- inherits both from draw-item and from the predefined
- MCL check-box-dialog-item classes. check-box-
- dialog-item (and the other MCL dialog items) have
- methods defined for drawing the radio buttons and
- performing the default actions associated with them.
- The drawing of the object is done by the method view-
- draw-contents, which gets called whenever a draw
- item needs to be drawn by virtue of the fact that
- draw-item is a subclass of dialog-item. By this same
- device, we can define the default mouse-down event
- handlers for draw items to include the mouse-down
- behavior of the dialog items simply by:
-
- (defmethod mouse-down ((item draw-item) where)
- (dialog-item-action item))
-
- dialog-item-action is the MCL method that performs
- the action of a dialog item. The method for the dialog-
- item class itself does nothing. The one for the check-
- box-dialog-item does the checking or unchecking of
- the box, depending on its state. And so on.
-
- If we want to define a brand new kind of item, we can
- do this as follows:
-
- (defclass brand-new-kind-of-item (draw-item)
- ())
-
- Suppose we want it to display as a red rectangle. Then
- we must define a view-draw-contents as follows:
-
- (defmethod view-draw-contents ((item brand-new-kind-of-item))
- (with-port (wptr item)
- (with-fore-color *red-color*
- (#_FrameRect :ptr (slot-value item 'rectangle))))
- (call-next-method))
-
- If we want it to beep when someone clicks on it, then
- we define something like:
-
- (defmethod mouse-down ((item brand-new-kind-of-item) where)
- (ed-beep))
-
- The complete source code provides a detailed view of
- how this works and looks together.
-
-
- end of file All about the Mini-App.text
- -----------------------------------------------
-